{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению\n",
    "Автор материала: программист-исследователь Mail.ru Group, старший преподаватель Факультета Компьютерных Наук ВШЭ Юрий Кашницкий. Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center>Тема 2. Визуальный анализ данных с Python\n",
    "## <center>Часть 2. Пример визуального анализа данных\n",
    "\n",
    "Считываем в DataFrame знакомые нам по [первой статье](https://habrahabr.ru/company/ods/blog/322626/) данные по оттоку клиентов телеком-оператора. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import division, print_function\n",
    "\n",
    "# отключим всякие предупреждения Anaconda\n",
    "import warnings\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "pd.set_option(\"display.max.columns\", 100)\n",
    "import pylab as plt\n",
    "\n",
    "%matplotlib inline\n",
    "import seaborn as sns\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "plt.rcParams[\"figure.figsize\"] = (10, 8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv(\"../../data/telecom_churn.csv\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим, все ли нормально считалось – посмотрим на первые 5 строк (метод `head`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Число строк (клиентов) и столбцов (признаков):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на признаки и убедимся, что пропусков ни в одном из них нет – везде по 3333 записи."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Описание признаков\n",
    "\n",
    "|  Название  | Описание | Тип |\n",
    "|---         |--:       |     |\n",
    "| **State** | Буквенный код штата | номинальный |\n",
    "| **Account length** | Как долго клиент обслуживается компанией | количественный |\n",
    "| **Area code** | Префикс номера телефона | количественный  |\n",
    "| **International plan** | Международный роуминг  (подключен/не подключен) | бинарный |\n",
    "| **Voice mail plan** | Голосовая почта (подключена/не подключена) | бинарный |\n",
    "| **Number vmail messages** | Количество голосовых сообщений | количественный |\n",
    "| **Total day minutes** |  Общая длительность разговоров днем | количественный |\n",
    "| **Total day calls** | Общее количество звонков днем | количественный |\n",
    "| **Total day charge** | Общая сумма оплаты за услуги днем | количественный |\n",
    "| **Total eve minutes** | Общая длительность разговоров вечером | количественный |\n",
    "| **Total eve calls** | Общее количество звонков вечером | количественный |\n",
    "| **Total eve charge** | Общая сумма оплаты за услуги вечером | количественный |\n",
    "| **Total night minutes** | Общая длительность разговоров ночью | количественный |\n",
    "| **Total night calls** | Общее количество звонков ночью | количественный |\n",
    "| **Total night charge** | Общая сумма оплаты за услуги ночью | количественный |\n",
    "| **Total intl minutes** | Общая длительность международных разговоров | количественный |\n",
    "| **Total intl calls** | Общее количество международных разговоров | количественный |\n",
    "| **Total intl charge** | Общая сумма оплаты за международные разговоры | количественный |\n",
    "| **Customer service calls** | Число обращений в сервисный центр | количественный |\n",
    "\n",
    "Целевая переменная: **Churn** – Признак оттока, бинарный (1 – потеря клиента, то есть отток). Потом мы будем строить модели, прогнозирующие этот признак по остальным, поэтому мы и назвали его целевым. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение целевого класса – оттока клиентов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"Churn\"].value_counts()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"Churn\"].value_counts().plot(kind=\"bar\", label=\"Churn\")\n",
    "plt.legend()\n",
    "plt.title(\"Распределение оттока клиентов\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выделим следующие группы признаков (среди всех кроме *Churn* ):\n",
    " - бинарные: *International plan*, *Voice mail plan*\n",
    " - категориальные: *State*\n",
    " - порядковые: *Customer service calls*\n",
    " - количественные: все остальные"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на корреляции количественных признаков. По раскрашенной матрице корреляций видно, что такие признаки как *Total day charge* считаются по проговоренным минутам (*Total day minutes*). То есть 4 признака можно выкинуть, они не несут полезной информации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corr_matrix = df.drop(\n",
    "    [\"State\", \"International plan\", \"Voice mail plan\", \"Area code\"], axis=1\n",
    ").corr()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.heatmap(corr_matrix);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на распределения всех интересующих нас количественных признаков. На бинарные/категориальные/порядковые признакие будем смотреть отдельно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "features = list(\n",
    "    set(df.columns)\n",
    "    - set(\n",
    "        [\n",
    "            \"State\",\n",
    "            \"International plan\",\n",
    "            \"Voice mail plan\",\n",
    "            \"Area code\",\n",
    "            \"Total day charge\",\n",
    "            \"Total eve charge\",\n",
    "            \"Total night charge\",\n",
    "            \"Total intl charge\",\n",
    "            \"Churn\",\n",
    "        ]\n",
    "    )\n",
    ")\n",
    "\n",
    "df[features].hist(figsize=(20, 12));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что большинство признаков распределены нормально. Исключения – число звонков в сервисный центр (*Customer service calls*) (тут больше подходит пуассоновское распределение) и число голосовых сообщений (*Number vmail messages*, пик в нуле, т.е. это те, у кого голосовая почта не подключена). Также смещено распределение числа международных звонков (*Total intl calls*)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Еще полезно строить вот такие картинки, где на главной диагонали рисуются распределения признаков, а вне главной диагонали – диаграммы рассеяния для пар признаков. Бывает, что это приводит к каким-то выводам, но в данном случае все примерно понятно, без сюрпризов. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df[features + [\"Churn\"]], hue=\"Churn\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "**Дальше посмотрим, как признаки связаны с целевым – с оттоком.**\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим boxplot-ы, описывающее статистики распределения количественных признаков в двух группах: среди лояльных и ушедших клиентов. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(nrows=3, ncols=4, figsize=(16, 10))\n",
    "\n",
    "for idx, feat in enumerate(features):\n",
    "    sns.boxplot(x=\"Churn\", y=feat, data=df, ax=axes[int(idx / 4), idx % 4])\n",
    "    axes[int(idx / 4), idx % 4].legend()\n",
    "    axes[int(idx / 4), idx % 4].set_xlabel(\"Churn\")\n",
    "    axes[int(idx / 4), idx % 4].set_ylabel(feat);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На глаз наибольшее отличие мы видим для признаков *Total day minutes*, *Customer service calls* и *Number vmail messages*. Впоследствии мы научимся определять важность признаков в задаче классификации с помощью случайного леса (или градиентного бустинга), и окажется, что первые два – действительно очень важные признаки для прогнозирования оттока."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим отдельно на картинки с распределением кол-ва проговоренных днем минут среди лояльных/ушедших. Слева - знакомые нам боксплоты, справа – сглаженные гистограммы распределения числового признака в двух группах (скорее просто красивая картинка, все и так понятно по боксплоту).\n",
    "\n",
    "Интересное **наблюдение:** в среднем ушедшие клиенты больше пользуются связью. Возможно, они недовольны тарифами, и одной из мер борьбы с оттоком будет понижение тарифных ставок (стоимости мобильной связи). Но это уже компании надо будет проводить дополнительный экономический анализ, действительно ли такие меры будут оправданы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, axes = plt.subplots(1, 2, sharey=True, figsize=(16, 6))\n",
    "\n",
    "sns.boxplot(x=\"Churn\", y=\"Total day minutes\", data=df, ax=axes[0])\n",
    "sns.violinplot(x=\"Churn\", y=\"Total day minutes\", data=df, ax=axes[1]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь изобразим распределение числа обращений в сервисный центр (такую картинку мы строили в первой статье). Тут уникальных значений признака не много (признак можно считать как количественным целочисленным, так и порядковым), и наглядней изобразить распределение с помощью `countplot`. **Наблюдение:** доля оттока сильно возрастает начиная с 4 звонков в сервисный центр."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(x=\"Customer service calls\", hue=\"Churn\", data=df);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на связь бинарных признаков *International plan* и *Voice mail plan* с оттоком. **Наблюдение**:  когда роуминг подключен, доля оттока намного выше, т.е. наличие междунароного роуминга – сильный признак. Про голосовую почту такого нельзя сказать. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, axes = plt.subplots(1, 2, sharey=True, figsize=(16, 6))\n",
    "\n",
    "sns.countplot(x=\"International plan\", hue=\"Churn\", data=df, ax=axes[0])\n",
    "sns.countplot(x=\"Voice mail plan\", hue=\"Churn\", data=df, ax=axes[1]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наконец, посмотрим, как с оттоком связан категориальный признак *State*. С ним уже не так приятно работать, поскольку число уникальных штатов довольно велико – 51. Можно в начале построить сводную табличку или посчитать процент оттока для каждого штата. Но мы видим, что данных по каждом штату по отдельности маловато (ушедших – всего от 3 до 17), поэтому, возможно, признак *State* впоследствии не стоит добавлять в модели классификации из-за риска *переобучения* (но мы это будем проверять на *кросс-валидации*, stay tuned!)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(df[\"State\"], df[\"Churn\"]).T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Доли оттока для каждого штата:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.groupby([\"State\"])[\"Churn\"].agg([np.mean]).sort_values(by=\"mean\", ascending=False).T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что в Нью-Джерси и Калифорнии доля оттока выше 25%, а на Гавайях и в Аляске меньше 5%. Но эти выводы построены на слишком скромной статистике и возможно, это просто особенности имеющихся данных (тут можно и гипотезы попроверять про корреляции Мэтьюса и Крамера, но это уже за рамками данной статьи)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наконец построим t-SNE представление данных. Название метода сложное – t-distributed Stohastic Neighbor Embedding, математика тоже крутая (и вникать в нее не будем), но основная идея проста, как дверь: найдем такое отображение из многомерного признакового пространства на плоскость (или в 3D, но почти всегда выбирают 2D), чтоб точки, которые были далеко друг от друга, на плоскости тоже оказались удаленными, а близкие точки – также отобразились на близкие. То есть neighbor embedding – это своего рода поиск нового представления данных, при котором сохраняется соседство. \n",
    "\n",
    "Немного деталей: выкинем штаты и признак оттока, бинарные Yes/No-признаки переведем в числа (при помощи [`pandas.Series.map`](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.map.html)). Также нужно масштабировать выборку – из каждого признака вычесть его среднее и поделить на стандартное отклонение, это делае `StandardScaler`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.manifold import TSNE\n",
    "from sklearn.preprocessing import StandardScaler"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# преобразуем все признаки в числовые, выкинув штаты\n",
    "X = df.drop([\"Churn\", \"State\"], axis=1)\n",
    "X[\"International plan\"] = X[\"International plan\"].map({\"Yes\": 1, \"No\": 0})\n",
    "X[\"Voice mail plan\"] = X[\"Voice mail plan\"].map({\"Yes\": 1, \"No\": 0})\n",
    "\n",
    "scaler = StandardScaler()\n",
    "X_scaled = scaler.fit_transform(X)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "tsne = TSNE(random_state=17)\n",
    "tsne_representation = tsne.fit_transform(X_scaled)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(tsne_representation[:, 0], tsne_representation[:, 1]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Раскрасим полученное t-SNE представление данных по оттоку (синие – лояльные, оранжевые – ушедшие клиенты)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(\n",
    "    tsne_representation[:, 0],\n",
    "    tsne_representation[:, 1],\n",
    "    c=df[\"Churn\"].map({0: \"blue\", 1: \"orange\"}),\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что ушедшие клиенты преимущественно \"кучкуются\" в некоторых областях признакового пространства. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Чтоб лучше понять картинку, можно также раскрасить ее по остальным бинарным признакам – по роумингу и голосовой почте. Синие участки соответствуют объектам, обладающим этим бинарным признаком."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, axes = plt.subplots(1, 2, sharey=True, figsize=(16, 6))\n",
    "\n",
    "axes[0].scatter(\n",
    "    tsne_representation[:, 0],\n",
    "    tsne_representation[:, 1],\n",
    "    c=df[\"International plan\"].map({\"Yes\": \"blue\", \"No\": \"orange\"}),\n",
    ")\n",
    "axes[1].scatter(\n",
    "    tsne_representation[:, 0],\n",
    "    tsne_representation[:, 1],\n",
    "    c=df[\"Voice mail plan\"].map({\"Yes\": \"blue\", \"No\": \"orange\"}),\n",
    ")\n",
    "axes[0].set_title(\"International plan\")\n",
    "axes[1].set_title(\"Voice mail plan\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь понятно, что, например, много ушедших клиентов кучкуется в левом кластере людей с поключенным роумингом, но без голосовой почты. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Напоследок отметим минусы t-SNE (да, по нему тоже лучше писать отдельную статью):\n",
    " - большая вычислительная сложность. Вот эта реализация sklearn скорее всего не поможет в Вашей реальной задаче, на выборках побольше стоит посмотреть в сторону [Multicore-TSNE](https://github.com/DmitryUlyanov/Multicore-TSNE);\n",
    " - картинка может сильно поменяться при изменении `random seed`, это усложняет интерпретацию. [Вот](http://distill.pub/2016/misread-tsne/) хороший тьюториал по t-SNE. Но в целом по таким картинкам не стоит делать далеко идущих выводов – не стоит гадать по кофейной гуще. Иногда что-то бросается в глаза и подтверждается при изучении, но это не часто происходит.\n",
    " \n",
    "Вот еще пара картинок. С помощью t-SNE можно действительно получить хорошее представление о данных (как в случае с рукописными цифрами, [вот](https://colah.github.io/posts/2014-10-Visualizing-MNIST/) хорошая статья), так и просто нарисовать елочную игрушку. \n",
    "\n",
    "<img src='../../img/tsne_mnist.png' />\n",
    "\n",
    "<img src='../../img/tsne_christmas_toy.jpg'/>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
